#include "VideoCapture.h"

LRESULT PASCAL StatusProc(HWND hWnd, int nID, LPSTR lpText) 
{ 
    if (!hWnd) 
        return FALSE; 
	VideoCapture *app=(VideoCapture*)GetWindowLong(hWnd,GWL_USERDATA);
	if(!app)
		return FALSE;
	return app->OnStatus(nID,lpText);
}
LRESULT PASCAL ErrorProc(HWND hWnd, int nID, LPSTR lpText) 
{ 
    if (!hWnd) 
        return FALSE; 
	VideoCapture *app=(VideoCapture*)GetWindowLong(hWnd,GWL_USERDATA);
	if(!app)
		return FALSE;
	return app->OnError(nID,lpText);
}
LRESULT PASCAL FrameProc(HWND hWnd, LPVIDEOHDR lpVHdr)
{
    if (!hWnd) 
        return FALSE;
	VideoCapture *app=(VideoCapture*)GetWindowLong(hWnd,GWL_USERDATA);
	if(!app)
		return FALSE;
	return app->OnFrame(lpVHdr);
}

BOOL VideoCapture::create()				
{
	FlatApplication::create();
	name=_T("VideoCapture");
	display.width=1024;
	display.height=768;
	display.bpp=32;
	clearColor=INT_WHITE;
	//frame_thread.priority=THREAD_PRIORITY_BELOW_NORMAL;
	error_str="";
	status_str="";

	frame_thread.set(this, "FrameThread");
	grab_thread.set(this, "GrabThread");
	save_thread.set(this, "SaveThread");
	connect_thread.set(this, "ConnectThread");
	play_thread.set(this, "PlayThread");

	InitInfo info;
	info.read(INIT_FILE,_T("="),_T("\n"),_T(" \t"));
	device_to_connect=info.getInt(_T("device_index"),DEFAULT_DEVICE);
	frame_width=info.getInt(_T("video_width"),DEFAULT_WIDTH);
	frame_height=info.getInt(_T("video_height"),DEFAULT_HEIGHT);
	frame_comps=info.getInt(_T("video_comp"),DEFAULT_COMPS);
	fps=info.getInt(_T("video_fps"),DEFAULT_FPS);
	quad_dim=info.getInt(_T("motion_dim"),DEFAULT_QUAD);
	motion_thres=(float)info.getDouble(_T("motion_thres"),DEFAULT_THRES);
	history=info.getInt(_T("motion_history"),DEFAULT_HISTORY);
	jpg_quality=info.getInt(_T("jpeg_quality"),DEFAULT_JPEG);
	observe_only=info.getInt(_T("observe_only"),FALSE);
	int nZones = info.getInt(_T("zones"),0);
	if(nZones>0)
	{
		String str = "zone";
		Tokenizer t;
		for(int i=0;i<nZones;i++)
		{
			CSTR data=info.get(str+format.toString(i), NULL);
			if(data)
			{
				t.set(data);
				if(t.length>=4)
					zones.add()->set(format.toInt(t[0]), format.toInt(t[1]), format.toInt(t[2]), format.toInt(t[3]));
			}
		}
	}
	quad_size=quad_dim*quad_dim;
	sleep_time=1000/fps;
	video_format=NULL;
	_tcscpy(output_dir,"capture\\");
	_tmkdir(output_dir);
	folder.set(output_dir);
	quad_dx=quad_dy=0;
	ClearStruct(date);
	initFrame();

	bCapture.text=_T("Capture");
	bCapture.borderLine=1;
	bCapture.borderColor=INT_DGRAY;
	bCapture.activeLine=2;
	bCapture.font=&font;
	bCapture.fontColor=INT_BLACK;
	bCapture.margin.set(5,1);
	bCapture.bgColor=0xFFDDDDDD;
	bCapture.overColor=INT_WHITE;
	bCapture.pressColor=INT_WCYAN;
	bCapture.adjustSize=TRUE;
	bCapture.checkable=TRUE;
	bCapture.location.set(10,10);

	chDevice.alignx=DIR_LEFT;
	chDevice.margin.set(10,1);
	chDevice.bgColor=INT_WHITE;
	chDevice.fontColor=INT_BLACK;
	chDevice.font=&font;
	chDevice.stick(DIR_RIGHT,5,0,&bCapture);

	bPlay.text=_T("Play");
	bPlay.copy(bCapture);
	bPlay.stick(DIR_RIGHT,5,0,&chDevice);
	
	bLoop.text=_T("Loop");
	bLoop.copy(bCapture);
	bLoop.stick(DIR_RIGHT,5,0,&bPlay);

	bStop.text=_T("Stop");
	bStop.copy(bCapture);
	bStop.checkable=FALSE;
	bStop.stick(DIR_RIGHT,5,0,&bLoop);

	iFrame.image=&read_image;
	iFrame.borderLine=1;
	iFrame.borderColor=INT_DGRAY;
	iFrame.adjustSize=TRUE;
	iFrame.stick(DIR_DOWN,0,5,&bCapture);

	iDiff.copy(iFrame);
	iDiff.image=&diff;
	iDiff.size.set(320,240);
	iDiff.stick(DIR_RIGHT,5,0,&iFrame);

	graph.grid.set(0,9);
	graph.formatX.precision=0;
	graph.formatY.precision=1;
	graph.formatY.exposant=FALSE;
	graph.marginChannel.set(20,40);
	graph.font=&font;
	graph.margin=10;
	//fg.graph.scale.set(1,(1);
	graph.smooth=TRUE;
	graph.shift=TRUE;
	//fg.graph.symAuto=symAuto;
	//fg.graph.logscaleX=log;
	graph.draw_to_index=TRUE;
	graph.limY.set(0.0,1.0);
	graph.size.set(320,240);
	graph.stick(DIR_DOWN,0,5,&iDiff);
	chFrame=graph.addChannel(_T("frame"));
	chMotion=graph.addChannel(_T("motion"),_T("frame"),INT_BLUE);
	chMotion->thick=2;

	add(&bCapture);
	add(&chDevice);
	add(&bPlay);
	add(&bLoop);
	add(&bStop);
	add(&iFrame);
	add(&iDiff);
	add(&graph);

	DEVICE *device;
	for (device_count=0;device_count<10;device_count++) 
	{
		device=devices+device_count;
		if(!capGetDriverDescription(device_count,
									device->name,sizeof(device->name),
									device->version,sizeof(device->version))) 
			break;
		chDevice.add(device->name);
	}
	chDevice.setSelectedIndex(device_to_connect);
	
	stop();
	return TRUE;
}
BOOL VideoCapture::glCreate()
{
	FlatApplication::glCreate();
	hWndC = capCreateCaptureWindow (
		_T("Capture"),	// window name if pop-up 
		WS_DISABLED,		// window style 
		0, 0, 0, 0,			// window position and dimensions
		(HWND) window.hWnd, 0 /* parent handle and child ID */); 
	if(!hWndC||!IsWindow(hWndC))
	{
		message(_T("Error creating capture window"));
		return FALSE;
	}
	// Register callbacks:
	SetWindowLong(hWndC,GWL_USERDATA,(LONG)this);
	capSetCallbackOnError(hWndC, ErrorProc); 
	capSetCallbackOnStatus(hWndC, StatusProc);
	capSetCallbackOnFrame(hWndC, FrameProc);

	audio_format.wFormatTag = WAVE_FORMAT_PCM;
	audio_format.nChannels = 2;                // Use stereo
	audio_format.nSamplesPerSec = 11025;
	audio_format.nAvgBytesPerSec = 22050;
	audio_format.nBlockAlign = 2;
	audio_format.wBitsPerSample = 8;
	audio_format.cbSize = 0;
	start(&connect_thread);
	return TRUE;
}
void VideoCapture::updateMask()
{
	const Rect<WORD> *zone;
	WORD left,right,top,bot;
	int w=levels_width;
	BOOL remove=FALSE;
	Lock lock(&cs);

	if(!zones.length)
	{
		mask.setValue(TRUE);
	}
	else
	{
		mask.clear();
		for(ListObject<Zone> *obj=zones.first;obj;obj=obj->next)
		{
			zone=&obj->value;
			left=zone->x;
			right=zone->x+zone->width;
			bot=levels_height-(zone->y+zone->height);
			top=levels_height-zone->y;
			for(int y=bot,start=bot*w+left;y<top;y++,start+=w)
				for(int x=left,m=start;x<right;x++,m++)
					mask.elems[m]=TRUE;
		}
	}
}
void VideoCapture::Disconnect()
{
	Lock lock1(&cs_frame);
	Lock lock2(&cs_capture);
	grab_time=op_time=frame_time=0;
	frame_count=0;
	motion_time=0;
	motion=0;
	status_id=0;
	error_id=0;
	frame=NULL;
	device_connected=-1;
	ClearStruct(caps);
	ClearStruct(status);
	ClearStruct(params);
	DELETE_MEMORY(video_format);
	video_format = NULL;
	header = NULL;
	palette = NULL;
	image_size=0;
	new_zone=NULL;
	frame_done=frame_prepared=frame_key=FALSE;
	inflate=1;
	if(bCapture.checked)
	{
		capDriverDisconnect(hWndC);
		bCapture.checked=FALSE;
	}
}
BOOL VideoCapture::Connect(int device_index)
{
	BOOL success=FALSE;
	Disconnect();
	
	if(capDriverConnect(hWndC,device_index))
	{
		capDriverGetCaps(hWndC,&caps,sizeof(caps));
		if(caps.wDeviceIndex==device_index)
		{
			device_connected=device_index;
			capSetAudioFormat(hWndC, &audio_format, sizeof(audio_format));
			capGetStatus(hWndC,&status,sizeof(status));
			capCaptureGetSetup(hWndC,&params,sizeof(params));
			params.dwRequestMicroSecPerFrame=1000*sleep_time;
			params.fYield=TRUE;
			params.fCaptureAudio=FALSE;
			capCaptureSetSetup(hWndC,&params,sizeof(params));
			capCaptureGetSetup(hWndC,&params,sizeof(params));
			fps=1000/(sleep_time=params.dwRequestMicroSecPerFrame/1000);
			DWORD dwSize = capGetVideoFormatSize(hWndC);
			NEW_MEMORY(video_format, BITMAPINFO, dwSize);
			header=&(video_format->bmiHeader);
			palette=video_format->bmiColors;
			capGetVideoFormat(hWndC, video_format, dwSize);
			if(video_format)
			{
				header->biWidth=frame_width;
				header->biHeight=frame_height;
				header->biBitCount=frame_comps*8;
				capSetVideoFormat(hWndC, video_format, dwSize);
				capGetVideoFormat(hWndC, video_format, dwSize);
				frame_width=(WORD)header->biWidth;
				frame_height=(WORD)header->biHeight;
				frame_comps=header->biBitCount/8;
				image_size=header->biSizeImage;
				if(initFrame())
				{
					chFrame->setSource(&motion_time,history);
					chMotion->setSource(&motion,history);
					start(&grab_thread);
					frame_timer.restart();
					success=TRUE;
				}
			}
		}
	}
	container.build();
	bPlay.checked=FALSE;
	bCapture.checked = success;
	return success;
}
BOOL VideoCapture::initFrame()
{
	if(!image.setSize(frame_width, frame_height))
		return FALSE;
	read_image = image;
	frame_size=frame_width*frame_height;
	levels_width=frame_width/quad_dim;
	levels_height=frame_height/quad_dim;
	levels_size=levels_width*levels_height;
	levels.setSize(levels_size,TRUE);
	mask.setSize(levels_size);
	mask.setValue(TRUE);
	diff.setSize(levels_width,levels_height,1);
	buffer.setSize(image.getSize());
	return TRUE;
}
void VideoCapture::glDestroy()
{
	FlatApplication::glDestroy();
}
void VideoCapture::destroy()
{
	Disconnect();
	InitInfo info;
	info.addInt(_T("device_index"),device_to_connect);
	info.addInt(_T("video_width"),frame_width);
	info.addInt(_T("video_height"),frame_height);
	info.addInt(_T("video_comp"),frame_comps);
	info.addInt(_T("video_fps"),fps);
	info.addInt(_T("motion_dim"),quad_dim);
	info.addDouble(_T("motion_thres"),motion_thres);
	info.addInt(_T("motion_history"),history);
	info.addInt(_T("jpeg_quality"),jpg_quality);
	info.addInt(_T("observe_only"),observe_only);
	info.addInt(_T("zones"),zones.length);
	if(zones.length)
	{
		String str="zone", data;
		ListObject<Zone> *obj;
		int i=0;
		for(obj=zones.first;obj;obj=obj->next)
		{
			Zone &zone = obj->value;
			data = format.toString(zone.x);
			data += ",";
			data += format.toString(zone.y);
			data += ",";
			data += format.toString(zone.width);
			data += ",";
			data += format.toString(zone.height);
			info.add(str+format.toString(i),data.toString());
			i++;
		}
	}
	info.write(INIT_FILE,_T("="),_T("\n"));
	FlatApplication::destroy();
	Lock lock(&cs_capture);
}
void VideoCapture::resetGraph()
{
}
DWORD VideoCapture::run(const ThreadInfo *info)
{
	int device=device_connected;
	PrecisionTimer timer;
	DWORD result=0;

	assert(info->threads < 2);
	if(info->threads > 1)
		return 0;

	if(info == &connect_thread)
	{
		if(Connect(chDevice.getSelectedIndex()))
		{
			if(!frame_thread.threads)
				start(&frame_thread);
			if(!save_thread.threads)
				start(&save_thread);
		}
	}
	else if(info == &frame_thread)
	{
		BYTE *pixel,*data,*ptr_buff,*ptr_diff,d,_lev_min,_lev_max,_diff_max;
		int h0,h1,w0,w1,h,w;
		DWORD l,*ptr_lev,tmp,i;
		for(;;)
		{
			if(!frames.length)
			{
				eventFrame.Wait();
				eventFrame.Reset();
			}
			if(device!=device_connected)
				break;
			Array *frame_data;
			BOOL success, waiting = FALSE;
			{
				Lock lock(&cs_capture);
				if(frames.length == 1)
				{
					waiting = TRUE;
					cs_frame.Enter();
				}
				timer.restart();
				frame_data = &frames.first->value;
				if(!frame_data)
					continue;
				success = readBitmap(video_format, frame_data->elems, frame_data->size, &image);
				frames.remove(0);
				if( waiting )
					cs_frame.Leave();
				if( !success )
					continue;
				data=image.data;
				read_image = image;
				switch(image.comps)
				{
				case 1:
					memcpy(buffer,data,image_size);
					break;
				case 3:
					for(h=0,ptr_buff=buffer,pixel=data;h<frame_height;h++)
						for(w=0;w<frame_width;w++,pixel+=3,ptr_buff++)
							*ptr_buff=(BYTE)(0.298912f*pixel[RED]+0.586611f*pixel[GREEN]+0.114478f*pixel[BLUE]);
					break;
				}
				ptr_lev=levels.elems;
				_lev_min=255;
				_lev_max=_diff_max=0;
				tmp=0;
				ptr_diff=diff.data;
				for(i=0,ptr_buff=buffer;i<frame_size;i++,ptr_buff++)
				{
					if(_lev_min>*ptr_buff)
						_lev_min=*ptr_buff;
					else if(_lev_max<*ptr_buff)
						_lev_max=*ptr_buff;
				}
				lev_min=_lev_min;
				lev_max=_lev_max;
				if(lev_min!=lev_max)
				{
					inflate=255.0f/(lev_max-lev_min);
					for(i=0;i<levels_size;i++)
					{
						l=0;
						h0=(i/levels_width)*quad_dim;
						h1=h0+quad_dim;
						for(int h=h0;h<h1;h++)
						{
							w0=(i%levels_width)*quad_dim;
							w1=w0+quad_dim;
							ptr_buff=buffer+(h*frame_width+w0);
							for(int w=w0;w<w1;w++,ptr_buff++)
								l+=*ptr_buff;
						}
						l=(DWORD)(inflate*l);
						tmp+=ptr_diff[i]=d=(BYTE)(ABS((LONG)l-(LONG)ptr_lev[i])/quad_size);
						if(mask.elems[i])
						{
							if(_diff_max<d)
								_diff_max=d;
						}
						ptr_lev[i]=l;
					}
					diff_max=_diff_max;
					diff_mid=(BYTE)(tmp/levels_size);
				}
				else
				{
					inflate = 0;
					diff_max=0;
					diff_mid=0;
				}
				GetLocalTime(&date);
				motion=DIFF_TO_MOTION*(diff_max-diff_mid);
				motion_time=frame_count;
				graph.addValues();
				graph.update();
				
				if(motion>motion_thres && !observe_only)
				{
					OUTPUT *output=outputs.add();
					_sntprintf(	output->name,MAX_PATH,
								"%stime_y%02d_m%02d_d%02d_h%02d_m%02d_s%02d_ms%03d.jpg",
								output_dir,date.wYear%100,date.wMonth,date.wDay,date.wHour,date.wMinute,date.wSecond,date.wMilliseconds);
					output->image = image;
					eventOutput.Signal();
				}
				diff.negative();
				op_time=(int)(1000*timer.time());
			}
			wake();
			Sleep(0);
		}
	}
	else if(info == &play_thread)
	{
		CSTR playFile;
		Image tmp;
		do
		{
			while(bPlay.checked&&(playFile=folder.next()))
			{
				_sntprintf(path,MAX_PATH,"%s%s",output_dir,playFile);
				if(readJPEG(path,&tmp))
				{
					_sntscanf(	playFile,MAX_PATH,_T("time_y%02d_m%02d_d%02d_h%02d_m%02d_s%02d_ms%03d.jpg"),
								&date.wYear,&date.wMonth,&date.wDay,&date.wHour,&date.wMinute,&date.wSecond,&date.wMilliseconds);
					read_image = tmp;
					wake();
					Sleep(sleep_time);
				}
			}
		}
		while(bLoop.checked);
		bPlay.checked=FALSE;
	}
	else if(info == &save_thread)
	{
		for(;;)
		{
			if(!outputs.length)
			{
				eventOutput.Wait();
				eventOutput.Reset();
			}
			OUTPUT *output=&outputs.first->value;
			writeJPEG(output->name,&output->image,jpg_quality);
			outputs.remove(0);
			Sleep(0);
		}
	}
	else if(info == &grab_thread)
	{
		while(device==device_connected)
		{
			timer.restart();
			capGrabFrame(hWndC);
			grab_time=(int)(1000*timer.time());
			Sleep(sleep_time);
		}
	}
	else
	{
		result=1;
	}

	return result;
}
void VideoCapture::reshape()
{
	FlatApplication::reshape();
}
BOOL VideoCapture::listen(WORD id, WORD code, const void *info)
{
	if(id==iFrame.id)
	{
		if(code==FLAT_ACTION_DEFAULT)
		{
			const Mouse *mouse=(Mouse*)info;
			const Rect<float> &rect=iFrame.getRect();
			quad_dx=(WORD)((mouse->x-rect.x)/quad_dim);
			quad_dy=(WORD)((mouse->y-rect.y)/quad_dim);
			if(mouse->lbutton)
			{
				if(!new_zone)
				{
					new_zone=zones.add();
					mouse_start.set(quad_dx,quad_dy);
				}
				mouse_end.set(quad_dx,quad_dy);
				new_zone->x=min(mouse_start.x,mouse_end.x);
				new_zone->y=min(mouse_start.y,mouse_end.y);
				new_zone->width=1+abs(mouse_start.x-mouse_end.x);
				new_zone->height=1+abs(mouse_start.y-mouse_end.y);
				updateMask();
			}
			else if(mouse->rbutton)
			{
				if(!mouse->move)
				{
					zones.remove();
					updateMask();
				}
			}
			else
			{
				new_zone=NULL;
				ListObject<Zone> *obj1,*obj2;
				BOOL remove1=FALSE,remove2=FALSE;
				Lock lock(&cs);

				for(obj1=zones.first;obj1;obj1=obj1->next)
				{
					if(remove1)
					{
						remove1=FALSE;
						zones.remove(obj1->prev);
					}
					for(obj2=obj1->next;obj2;obj2=obj2->next)
					{
						if(remove2)
						{
							remove2=FALSE;
							zones.remove(obj2->prev);
						}
						if(obj1->value.contain(obj2->value))
						{
							remove2=TRUE;
						}
						else if(obj2->value.contain(obj1->value))
						{
							remove1=TRUE;
							break;
						}
					}
					if(remove2)
					{
						remove2=FALSE;
						zones.remove(zones.last);
					}
				}
			}
		}
		return TRUE;
	}
	if(id==bStop.id)
	{
		if(code==FLAT_ACTION_DEFAULT)
		{
			stop();
		}
		return TRUE;
	}
	if(id==bCapture.id)
	{
		if(code==FLAT_ACTION_DEFAULT)
		{
			BOOL tmp=bCapture.checked;
			stop();
			if(tmp)
				start(&connect_thread);
		}
		return TRUE;
	}
	if(id==bPlay.id)
	{
		if(code==FLAT_ACTION_DEFAULT)
		{
			if(bPlay.checked)
			{
				Disconnect();
				start(&play_thread);
			}
		}
		return TRUE;
	}
	return FALSE;
}
void VideoCapture::update()
{
	//capGrabFrame(hWndC);
	//setUpdated(TRUE);
}
BOOL VideoCapture::OnStatus(int id, LPSTR text)
{
	wake();
	status_id=id;
	status_str=id?text:"";
	return TRUE;
}
BOOL VideoCapture::OnError(int id, LPSTR text)
{
	wake();
	error_id=id;
	error_str=id?text:"";
	return TRUE;
}
BOOL VideoCapture::OnFrame(LPVIDEOHDR newFrame)
{
	frame_count++;
	frame_time=(int)(1000*frame_timer.restart());
	frame=newFrame;
	frame_done=(newFrame->dwFlags&VHDR_DONE)!=0;
	frame_prepared=(newFrame->dwFlags&VHDR_PREPARED)!=0;
	frame_key=(newFrame->dwFlags&VHDR_KEYFRAME)!=0;
	if(!frame_done)
	{
		Lock lock(&cs_frame);
		Array *data=frames.add();
		data->copy(newFrame->lpData,newFrame->dwBufferLength);
		eventFrame.Signal();
	}
	return TRUE;
}
void VideoCapture::draw2Dpost()
{
	if(!bPlay.checked)
	{
		const Rect<float>& img_rect=iFrame.getRect();
		const Rect<WORD> *zone_rect;
		WORD left,top,width,height;
		Lock lock(&cs);

		for(ListObject<Zone> *obj=zones.first;obj;obj=obj->next)
		{
			zone_rect=&obj->value;
			left=(WORD)img_rect.x+quad_dim*zone_rect->x;
			top=(WORD)img_rect.y+quad_dim*zone_rect->y;
			width=quad_dim*zone_rect->width;
			height=quad_dim*zone_rect->height;
			drawBox(left,top,width,height,INT_RED,2);
		}
	}
	//glPixelZoom((float)quad_dim,(float)quad_dim);
	//drawImage(&diff,250,680,ALIGN_ADJUST);

	font.color=INT_BLACK;
	font.x=15;
	font.y=600;
	glPrint(&font,"Time     %02d/%02d/%02d %02d:%02d:%02d %03d",date.wYear%100,date.wMonth,date.wDay,date.wHour,date.wMinute,date.wSecond,date.wMilliseconds);
	font.y+=15;
	glPrint(&font,"Fps      %d/%d",frame_time?1000/frame_time:0,fps);
	font.y+=15;
	glPrint(&font,"Format   %d/%d/%d",frame_width,frame_height,frame_comps);
	font.y+=15;
	glPrint(&font,"Queue    %d/%d",frames.length,outputs.length);
	font.y+=15;
	glPrint(&font,"TimeOp   %3d/%3d",op_time,grab_time);
	font.y+=15;
	glPrint(&font,"Diff     %3d/%3d",diff_mid,diff_max);
	font.y+=15;
	glPrint(&font,"Lev      %3d/%3d",lev_min,lev_max);
	font.y+=15;
	glPrint(&font,"Inflate  %.3f",inflate);
	font.y+=15;
	glPrint(&font,"Error    %s",(CSTR)error_str);
}
int WINAPI _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow)
{
	VideoCapture app;
	return app.main(hInstance,hPrevInstance,lpCmdLine,nCmdShow);
}
